package edu.cmu.cs211.snake.date;

public class Date 
{
	private final int month;
	private final int day;
	private final int year;
	
	// used only for testing. DO NOT USE IN YOUR HASHCODE
	private int num_equal_calls = 0; 
	
	private static final int JANUARY = 1;
	private static final int FEBUARY = 2;
	private static final int MARCH = 3;
	private static final int APRIL = 4;
	private static final int MAY = 5;
	private static final int JUNE = 6;
	private static final int JULY = 7;
	private static final int AUGUST = 8;
	private static final int SEPTEMBER = 9;
	private static final int OCTOBER = 10;
	private static final int NOVEMBER = 11;
	private static final int DECEMBER = 12;
	
	/*
	 * This is a good hash function since:
	 * there are 12 months in total, collisions for mod 13 is very low
	 * there are 31 days in total, collisions for mod 61 is very low
	 * there are 2010 years in total, collisions for mod 2039 is very low
	 * we shift the mod results and add them together, thus the result hash number has a low collision rate
	 * 
	 * this hash function has around 50 collisions for 10000 items.
	 * 
	 * 
	 */
	public int hashCode()
	{
		return 1000000*(month%13) + 10000*(day % 61) + (year % 2039);
	}
	
	/**
	 * Constructs a new Date object with a random day, month and year
	 */	
	public Date()
	{
		month = randomInt(JANUARY, DECEMBER);
		year = randomInt(0, 9999);
		day = randomInt(1, getNumDaysInMonth(month, year));
	}

	/**
	 * Constructs a new Date object with the supplied day, month, and year
	 * 
	 * @param month
	 *            the month for this date object 
	 * @param day
	 *            the day for this date object 
	 * @param year
	 *            the year for this date object
	 * @throws IllegalArgumentException
	 *             if the month is not between <tt>JANUARY</tt> and <tt>DECEMEBR</tt>,
	 *             if the year is not between <tt>0</tt> and <tt>9999</tt>, or
	 *             if the day is not between <tt>1</tt> and the number of days in the
	 *             supplied month
	 */
	public Date(int month, int day, int year)
	{
		if(month < JANUARY || month > DECEMBER)
			throw new IllegalArgumentException("The month must be between " + JANUARY + " and " + DECEMBER + ".");
		if(year < 0 || year > 9999)
			throw new IllegalArgumentException("The year must be between 0 and 9999.");
		
		int numDaysInMonth = getNumDaysInMonth(month, year);
		
		if(day < 1 || day > numDaysInMonth)
			throw new IllegalArgumentException("The day must be between " + 1 + " and " + numDaysInMonth + " in " + monthToString() + " " + year + ".");
		
		this.month = month;
		this.day = day;
		this.year = year;
	}
	
	public int getMonth() 
	{
		return month;
	}

	public int getDay() 
	{
		return day;
	}

	public int getYear() 
	{
		return year;
	}
	
	public int getNumEqualCalls()
	{
		return num_equal_calls;
	}
	
	public boolean equals(Object obj) 
	{
		num_equal_calls++;
		
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof Date))
			return false;
		
		Date other = (Date) obj;
		return (day == other.day && month == other.month && year == other.year);
	}
	
	// returns a random int on the range [min, max]
	private int randomInt(int min, int max)
	{
		int diff = max - min + 1;
		double rand = Math.random()*diff + min;
		return (int)rand;	
	}
	
	// returns the number of days in that month, year
	private int getNumDaysInMonth(int month, int year)
	{
		switch(month)
		{
			case JANUARY:
			case MARCH:
			case MAY:
			case JULY:
			case AUGUST:
			case OCTOBER:
			case DECEMBER:
				return 31;
				
			case APRIL:
			case JUNE:
			case SEPTEMBER:
			case NOVEMBER:
				return 30;
				
			case FEBUARY:
				if(year % 4 == 0 && year % 100 != 0)
					return 29;
				else
					return 28;
				
			default:
				return 0;
		}
	}
	
	// returns a string representation of the month for this Date object
	private String monthToString()
	{
		switch(month)
		{
			case JANUARY: return "January";				
			case FEBUARY: return "Febuary";
			case MARCH: return "March";			
			case APRIL: return "April";
			case MAY: return "May";
			case JUNE: return "June";
			case JULY: return "July";
			case AUGUST: return "August";	
			case SEPTEMBER: return "September";
			case OCTOBER: return "October";
			case NOVEMBER: return "November";
			case DECEMBER: return "December";
			default: return "No Such Month";
		}
	}
	
	public String toString()
	{
		return monthToString() + " " + day + ", " + year;
	}
}
